package de.dailab.graphworld.world;

import java.awt.Color;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import de.dailab.graphworld.gui.GraphWorldGUI;
import de.dailab.graphworld.ontology.Edge;
import de.dailab.graphworld.ontology.Graph;
import de.dailab.graphworld.ontology.GraphWorldException;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.Team;
import de.dailab.jiactng.agentcore.AbstractAgentBean;
import de.dailab.jiactng.agentcore.IAgentNode;
import de.dailab.jiactng.agentcore.SimpleAgentNode;
import de.dailab.jiactng.agentcore.action.Action;
import de.dailab.jiactng.agentcore.action.DoAction;
import de.dailab.jiactng.agentcore.action.scope.ActionScope;
import de.dailab.jiactng.agentcore.environment.IEffector;
import de.dailab.jiactng.agentcore.lifecycle.LifecycleException;
import de.dailab.jiactng.agentcore.ontology.IActionDescription;
import de.dailab.jiactng.agentcore.ontology.IAgentDescription;

public class ExtendedGraphWorldAgentBean extends AbstractAgentBean implements IEffector {

    // action names
    /**
     * Move an agent. Parameters: Target Position, Returns: new Position
     */
    public static final String ACTION_MOVE = "GraphWorld.Move";
    /**
     * Take an amount of gold. Parameters: Amount of gold to take (Integer),
     * Returns: Amount of Gold taken from Node
     */
    public static final String ACTION_TAKE_GOLD = "GraphWorld.TakeGold";
    /**
     * Drop all carried gold. Parameters: -
     */
    public static final String ACTION_DROP_GOLD = "GraphWorld.DropGold";
    /**
     * Explore the world around the agent. Parameters: -, Returns: Collection of
     * graph nodes
     */
    public static final String ACTION_EXPLORE = "GraphWorld.Explore";
    /**
     * Receive a start position for an agent. Returns: Start Position
     */
    public static final String ACTION_INIT_POSITION = "GraphWorld.GetInitialPosition";
    /**
     * Receive a start position for an agent and assign the agent to a team.
     * Parameters: Team name, Returns: Start Position
     */
    public static final String ACTION_INIT_TEAM_POSITION = "GraphWorld.GetInitialTeamPosition";
    /**
     * Drop a "home" pheromone on an edge . Parameters: Edge target position,
     * Returns: Updated graph node
     */
    public static final String ACTION_DROP_HOME_PHEROMONE = "GraphWorld.DropHomePheromone";
    /**
     * Drop a "gold" pheromone. Parameters: Edge target position, Returns:
     * Updated graph node
     */
    public static final String ACTION_DROP_GOLD_PHEROMONE = "GraphWorld.DropGoldPheromone";
    private final Random random = new Random(System.identityHashCode(this));
    private static final String DEFAULTTEAMNAME = "noTeamName";
    // used by move action
    private int allowedMoveInterval = 1000;
    private Map<String, Long> agentsLastMove = new HashMap<String, Long>();
    // mapping to track which agent carries how much gold
    private Map<String, Integer> carryingGold = new HashMap<String, Integer>();
    // for team play, Map AgentID -> Team Name
    private Map<String, Team> teams = new HashMap<String, Team>();
    private Map<String, Team> teamMembers = new HashMap<String, Team>();
    //gui
    private GraphWorldGUI gui;
    //set by XML file
    //graph size
    private int size;
    //view range -> explore
    private int viewrange = 0;
    //max agents per node
    private int agentsPerNode = 1;
    //maximum amount of gold per node
    private int maxGoldPerNode = 1;
    //start at home position or at randomPosition
    private boolean startAtHome = false;
    //decay factor for pheromones, how fast pheromones will dissolve
    private double pheromoneDecay = 0.9;
    //the amount of gold pieces an agent can carry at most
    private int agentGoldCapacity = 1;

    @Override
    public void doInit() {
        // initialize new graph and write him in memory
        final Graph graph = new Graph(new Random(), size, maxGoldPerNode, pheromoneDecay);
        this.memory.write(graph);
        // add all nodes to memory too
        for (final Node node : (ArrayList<Node>) graph.getNodes()) {
            this.memory.write(node);
        }
    }

    @Override
    public void doStart() {
        // create gui
        this.gui = new GraphWorldGUI(false);
        // load created graph to gui
        this.gui.setGraph(this.memory.read(new Graph(new Random(), size)));
        // window close options
        this.gui.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                super.windowClosed(e);
                IAgentNode node = null;
                if ((node = ExtendedGraphWorldAgentBean.this.thisAgent.getAgentNode()) instanceof SimpleAgentNode) {
                    try {
                        ((SimpleAgentNode) node).shutdown();
                    } catch (LifecycleException lex) {
                        System.err.println("could not shut down this agent node: " + lex.getMessage());
                    }
                }
            }
        });
        // set visible
        this.gui.setVisible(true);
        // make it look nice
        this.refreshGUI();
    }

    @Override
    public void doStop() {
        // shut down everything
        if (this.gui != null) {
            this.gui.setVisible(false);
            this.gui.dispose();
            this.gui = null;
        }
    }

    private void refreshGUI() {
        // if gui exists
        if (this.gui != null) {
            // load actual graph from memory
            final Graph graph = this.memory.read(new Graph(new Random(), size));
            if (graph != null) {
                // refresh gui
                this.gui.update();
            } else {
                System.err.println("could not find gui");
            }
        }
    }

    private void initLocation(final DoAction action) {
        // create a template used for reading from agent's memory
        final Node tpl = new Node(null);
        tpl.agents.add(action.getSession().getOriginalAgentDescription());
        Node position = memory.read(tpl);

        if (log.isDebugEnabled()) {
            log.debug("agent (" + ((IAgentDescription) tpl.agents.get(0)).getAid() + ") position from memory = " + position);
        }

        /*
         * if a position was found where the agent is located, return this location 
         * otherwise search for free and collision free position and assign the agent to this position
         */
        if (position == null) {
            if (this.startAtHome) {
                position = this.memory.read(new Node(0));
            } else {
                do {
                    final int id = this.random.nextInt(size);
                    // read a node with random ID from graph
                    position = this.memory.read(new Node(id));
                } // do the loop, until a free location was found
                while (position.getGold() == 0 && (position.agents.size() >= agentsPerNode));
                // mark the location as occupied
            }
            position.agents.add(action.getSession().getOriginalAgentDescription());
        }
        if (log.isDebugEnabled()) {
            log.debug("assigned agent position = " + position);
        }
        // report the location of marked location
        this.returnResult(action, new Serializable[]{position});
    }

    /**
     * Returns the color the next new team gets.
     *
     * @return AWT color
     */
    private Color getNextTeamColor() {
        int numberOfTeams = teams.size();
        switch (numberOfTeams) {
            case 0:
                return Color.RED;
            case 1:
                return Color.GREEN;
            case 2:
                return Color.CYAN;
            case 3:
                return Color.ORANGE;
            default:
                return Color.RED;
        }
    }

    /**
     * Initializes team location and adds the agent to the specified team.
     *
     * @param action
     */
    private void initTeamLocation(final DoAction action) {
        final String teamName = (String) action.getParams()[0];
        log.info("Initializing new Agent for team " + teamName);
        Team team = teams.get(teamName);
        if (team == null) {
            team = new Team(teamName, this.getNextTeamColor());
            teams.put(teamName, team);
            gui.teamAdded(teamName);
        }
        teamMembers.put(action.getSession().getOriginalAgentDescription().getAid(), team);
        if (gui.getTeams() == null) {
            gui.setTeams(teamMembers);
        }
        initLocation(action);
    }

    private boolean exists(final Node n) {
        return (n.getID() < size && n.getID() > -1);
    }

    private Node read(final Node direction) {
        final Node changed = new Node(direction.getID());
        return this.memory.read(changed);
    }

    /**
     * Checks if an agent is on the specified node.
     *
     * @param node graph node
     * @param agent agent description
     * @return true if the agent is currently located on this node
     */
    private boolean isAgentAtPosition(final Node node, final IAgentDescription agent) {
        Node graphNode = this.read(node);
        return graphNode.agents.contains(agent);
    }

    private Node getAgentPosition(final IAgentDescription agent) {
        ArrayList<IAgentDescription> agents = new ArrayList<IAgentDescription>();
        agents.add(agent);
        Node template = new Node(null, null, null, null, agents);
        return this.memory.read(template);
    }

    // explores locations depenent on position and viewrange
    private void explore(final DoAction action) {

        try {
            // get location out of action and read it from memory
            final Node position = this.getAgentPosition(action.getSession().getOriginalAgentDescription());
            final Node tpl = new Node(position.getID());
            final Node graphPosition = this.memory.read(tpl);

            if (graphPosition == null) {
                this.returnFailure(action, new GraphWorldException("the position was not found in the graph: " + position, position));
                return;
            }

            final HashSet<Node> result = new HashSet<Node>();

            // viewrange only local (0)
            if (viewrange == 0) {
                result.add(graphPosition);

                // viewrange reaches one neighbor (1)
            } else if (viewrange == 1) {
                // TODO implementation required
                log.warn("Implementation for view range 1 required!");

                // viewrange reaches all neighbors (2)
            } else if (viewrange == 2) {
                result.add(graphPosition);
                for (Edge e : (ArrayList<Edge>) graphPosition.getEdges()) {
                    if (this.exists(e.getTarget())) {
                        Node pos = this.read(e.getTarget());
                        if (pos == null) {
                            log.warn("Position read from memory is null; original position: " + position + ", direction: " + e);
                        }
                        result.add(pos);
                    }
                }
            } else {
                this.returnFailure(action, new GraphWorldException("viewrange not set properly", position));
                return;
            }
            this.returnResult(action, new Serializable[]{result});
        } catch (final Exception e) {
            this.returnFailure(action, e);
        }

    }

    // updates agent movement
    private void move(final DoAction action) {
        try {
            // get movement nodes from action
            final Node position = this.getAgentPosition(action.getSession().getOriginalAgentDescription());
            final Node direction = (Node) action.getParams()[0];

            // get moving agent from action
            IAgentDescription movingAgent = action.getSession().getOriginalAgentDescription();

            // check for illegal move interval
            Long last = agentsLastMove.get(movingAgent.getAid());
            if (last == null) {
                agentsLastMove.put(movingAgent.getAid(), System.currentTimeMillis());
            } else {
                if ((last + allowedMoveInterval) > System.currentTimeMillis()) {
                    this.returnFailure(action, new GraphWorldException("Illegal move; move interval is " + allowedMoveInterval + " ms", position));
                    return;
                } else {
                    agentsLastMove.put(movingAgent.getAid(), System.currentTimeMillis());
                }
            }

            if (this.exists(direction)) {
                Node move = this.memory.read(new Node(direction.getID()));
                // check if the agent is actually on the start position he sent (exploit prevention)
                final Node clear = this.memory.read(new Node(position.getID()));
                if (this.isAgentAtPosition(clear, action.getSession().getOriginalAgentDescription())) {
                    // check for collisions
                    if (move.agents.size() >= agentsPerNode) {
                        this.returnFailure(action, new GraphWorldException("this position is already in use.", move));
                    } else if (clear.isNeighbor(direction)) {
                        // update location of the moving agent
                        move.agents.add(action.getSession().getOriginalAgentDescription());
                        // clear old position of the requesting agent

                        clear.agents.remove(action.getSession().getOriginalAgentDescription());
                        memory.update(clear, clear);
                        // return the current location of the agent
                        this.returnResult(action, new Serializable[]{move});
                    } else {
                        this.returnFailure(action, new GraphWorldException("Target position is not a neighbour of current position!", new Node(direction.getID())));
                    }
                } else {
                    this.returnFailure(action, new GraphWorldException("Start position was wrong!", new Node(position.getID())));
                }
            } else {
                final GraphWorldException e = new GraphWorldException("the new position does not exist in the graph!", new Node(direction.getID()));
                this.returnFailure(action, e);
            }
        } catch (final Exception e) {
            this.returnFailure(action, e);
        }
    }

    // agent collects gold
    private void takeGold(final DoAction origin) {
        try {
            // get amount to take from parameters
            final Integer amountToTake = (Integer) origin.getParams()[0];
            //get node from memory
            final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());

            int capacity = this.agentGoldCapacity;
            String agentId = origin.getSession().getOriginalAgentDescription().getAid();

            if (carryingGold.containsKey(agentId)) {
                capacity -= carryingGold.get(agentId).intValue();
            }

            if (amountToTake > capacity) {
                this.returnFailure(origin, new GraphWorldException("Agent cannot carry that much gold", position));
                return;
            }

            if (this.exists(position)) {
                final Node read = this.memory.read(new Node(position.getID()));
                // update gold
                if (read.getGold() > 0) {
                    int taken = amountToTake;
                    if (read.getGold() < amountToTake) {
                        taken = read.getGold();
                        read.setGold(0);
                    } else {
                        read.setGold(read.getGold() - amountToTake);
                    }
                    if (carryingGold.containsKey(agentId)) {
                        int newAmount = carryingGold.get(agentId) + Integer.valueOf(taken);
                        carryingGold.put(agentId, newAmount);
                    } else {
                        carryingGold.put(agentId, Integer.valueOf(taken));
                    }
                    this.returnResult(origin, new Serializable[]{Integer.valueOf(taken)});
                } else {
                    this.returnFailure(origin, new GraphWorldException("location does not contain any gold", position));
                }
            } else {
                final GraphWorldException e = new GraphWorldException("location does not exist", position);
                this.returnFailure(origin, e);
            }
        } catch (final Exception e) {
            this.returnFailure(origin, e);
        }
    }

    // agent drops gold
    private void dropGold(final DoAction origin) {

        try {
            // get position from action
            final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());
            IAgentDescription agent = origin.getSession().getOriginalAgentDescription();

            if (!carryingGold.containsKey(agent.getAid())) {
                this.returnFailure(origin, new GraphWorldException("Agent was not carrying any gold", position));
                return;
            }

            if (this.exists(position)) {
                final Node read = this.memory.read(new Node(position.getID()));
                if (read.getID() == 0) {
                    // this is a drop at HOME location
                    if (agent != null) {
                        int carried = carryingGold.remove(agent.getAid()).intValue();
                        gui.collected += carried;
                        Team team = this.teamMembers.get(agent.getAid());
                        if (team != null) {
                            gui.increaseTeamScore(team.getTeamName(), carried);
                        }
//						refreshGUI();
                    }
                    this.returnResult(origin, new Serializable[]{});
                } else {
                    // drop on non-home location => just increase gold on node by 1

                    int carried = carryingGold.remove(agent.getAid()).intValue();
                    read.setGold(read.getGold() + carried);
                }
            } else {
                final GraphWorldException e = new GraphWorldException("location does not exist", position);
                this.returnFailure(origin, e);
            }
        } catch (final Exception e) {
            this.returnFailure(origin, e);
        }
    }

    @Override
    public void doAction(final DoAction doAction) throws Exception {

        final String aname = doAction.getAction().getName();

        if (ExtendedGraphWorldAgentBean.ACTION_MOVE.equals(aname)) {
            this.move(doAction);
        } else if (ExtendedGraphWorldAgentBean.ACTION_INIT_POSITION.equals(aname)) {
            this.initLocation(doAction);
        } else if (ExtendedGraphWorldAgentBean.ACTION_EXPLORE.equals(aname)) {
            this.explore(doAction);
        } else if (ExtendedGraphWorldAgentBean.ACTION_TAKE_GOLD.equals(aname)) {
            this.takeGold(doAction);
        } else if (ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD.equals(aname)) {
            this.dropGold(doAction);
        } else if (ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD_PHEROMONE.equals(aname)) {
            this.dropGoldPheromone(doAction);
        } else if (ExtendedGraphWorldAgentBean.ACTION_DROP_HOME_PHEROMONE.equals(aname)) {
            this.dropHomePheromone(doAction);
        } else if (ExtendedGraphWorldAgentBean.ACTION_INIT_TEAM_POSITION.equals(aname)) {
            this.initTeamLocation(doAction);
        }
//		this.refreshGUI();
    }

    // drop of pheromones => ONLY USED BY SWARM AGENT
    private void dropHomePheromone(final DoAction origin) {
        String teamName = DEFAULTTEAMNAME;
        Team team = this.teamMembers.get(origin.getSession().getOriginalAgentDescription().getAid());
        if (team != null) {
            teamName = team.getTeamName();
        }
        try {
            // get position and target from action
            final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());
            final Node target = (Node) origin.getParams()[0];
            if (this.exists(position) && this.exists(target)) {

                final Node src = this.memory.read(new Node(position.getID()));
                // look for correct edge to drop on
                for (Edge e : src.getEdges()) {
                    if (e.getTarget().getID() == target.getID()) {
                        e.dropHomePheromone(teamName);
                        break;
                    }
                }
                this.returnResult(origin, new Serializable[]{});
            } else {
                if (!this.exists(position)) {
                    final GraphWorldException e = new GraphWorldException("location does not exist", position);
                    this.returnFailure(origin, e);
                } else {
                    final GraphWorldException e = new GraphWorldException("location does not exist", target);
                    this.returnFailure(origin, e);
                }
            }
        } catch (final Exception e) {
            this.returnFailure(origin, e);
        }

    }

    // see dropHomePheromone
    private void dropGoldPheromone(final DoAction origin) {
        String teamName = DEFAULTTEAMNAME;
        Team team = this.teamMembers.get(origin.getSession().getOriginalAgentDescription().getAid());
        if (team != null) {
            teamName = team.getTeamName();
        }
        try {
            final Node position = this.getAgentPosition(origin.getSession().getOriginalAgentDescription());
            final Node target = (Node) origin.getParams()[0];
            if (this.exists(position) && this.exists(target)) {
                if (!this.isAgentAtPosition(position, origin.getSession().getOriginalAgentDescription())) {
                    this.returnFailure(origin, new GraphWorldException("Agent is not on this position!", new Node(position.getID())));
                    return;
                }
                final Node src = this.memory.read(new Node(position.getID()));
                for (Edge e : src.getEdges()) {
                    if (e.getTarget().getID() == target.getID()) {
                        e.dropGoldPheromone(teamName);
                        break;
                    }
                }
                this.returnResult(origin, new Serializable[]{});
            } else {
                if (!this.exists(position)) {
                    final GraphWorldException e = new GraphWorldException("location does not exist", position);
                    this.returnFailure(origin, e);
                } else {
                    final GraphWorldException e = new GraphWorldException("location does not exist", target);
                    this.returnFailure(origin, e);
                }
            }
        } catch (final Exception e) {
            this.returnFailure(origin, e);
        }
    }

    @Override
    public List<? extends IActionDescription> getActions() {
        List<Action> actions = new ArrayList<Action>();

        actions.add(new Action(ACTION_INIT_POSITION, this, new Class[]{}, new Class[]{Node.class}));
        actions.add(new Action(ACTION_INIT_TEAM_POSITION, this, new Class[]{String.class}, new Class[]{Node.class}));
        actions.add(new Action(ACTION_EXPLORE, this, new Class[]{}, new Class[]{Collection.class}));
        actions.add(new Action(ACTION_MOVE, this, new Class[]{Node.class}, new Class[]{Node.class}));
        actions.add(new Action(ACTION_TAKE_GOLD, this, new Class[]{Integer.class}, new Class[]{Integer.class}));
        actions.add(new Action(ACTION_DROP_GOLD, this, new Class[]{}, new Class[]{}));
        actions.add(new Action(ACTION_DROP_GOLD_PHEROMONE, this, new Class[]{Node.class}, new Class[]{}));
        actions.add(new Action(ACTION_DROP_HOME_PHEROMONE, this, new Class[]{Node.class}, new Class[]{}));

        for (Action a : actions) {
            a.setScope(ActionScope.NODE);
        }

        return actions;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public int getViewrange() {
        return viewrange;
    }

    public void setViewrange(int viewrange) {
        this.viewrange = viewrange;
    }

    public int getAgentsPerNode() {
        return agentsPerNode;
    }

    public void setAgentsPerNode(int agentsPerNode) {
        this.agentsPerNode = agentsPerNode;
    }

    public int getMaxGoldPerNode() {
        return maxGoldPerNode;
    }

    public void setMaxGoldPerNode(int maxGoldPerNode) {
        this.maxGoldPerNode = maxGoldPerNode;
    }

    public boolean isStartAtHome() {
        return startAtHome;
    }

    public void setStartAtHome(boolean startAtHome) {
        this.startAtHome = startAtHome;
    }

    public double getPheromoneDecay() {
        return pheromoneDecay;
    }

    public void setPheromoneDecay(double pheromoneDecay) {
        this.pheromoneDecay = pheromoneDecay;
    }

    public int getAgentGoldCapacity() {
        return agentGoldCapacity;
    }

    public void setAgentGoldCapacity(int agentGoldCapacity) {
        this.agentGoldCapacity = agentGoldCapacity;
    }

    @Override
    public void execute() {
        this.refreshGUI();
    }
}
